# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Hint class for calculating possible moves. Requires just stack lists from Game.
"""
__docformat__ = "restructuredtext en"

from collections import namedtuple
import logging
import gc

LOGGER = logging.getLogger('PySolFC.Hint')
AHINT = namedtuple('HintTuple', ('score', 'pos', 'ncards', 'from_stack',
                                'to_stack', 'color', 'forced_move',))

from .packlib.info import KING, ACE, TWO

class AClonedStack(object):
    """
    Create a Stack shallow copy.
    """

    def __init__(self, stack):
        """
        __init__(stack:Stack) -> None

        Initialize clone of **stack**.
        """
        self.__class__ = type(stack)  # copy class identity
        stack.copyModel(self)  # copy model data (reference copy)

    @staticmethod
    def clone(stack, cards):
        """
        clone(stack:Stack, cards:seq(Card ...))

        Returns clone of **stack** with shallow copy of **cards**.
        """
        clone_stack = AClonedStack(stack)
        stack.copyModel(clone_stack)
        clone_stack.cards = cards[:]
        return clone_stack


class AbstractHint(object):
    """
    A hint framework for providing possible atomic moves for a solitaire game.
    """
    K = KING + 1  # score value so scores look nicer

    # scores for flip and deal moves. (0..10000)
    SCORE_FLIP = 100000
    SCORE_DEAL = 0

    _color = None
    _score = 0
    _hints = []

    def __init__(self, stacks, level):
        """
        __init__(stacks:dict, level:int) -> None

        *Parameters*
          level
            0 - user mode - show hint (key `H')
            1 - debug mode - show hint and score value (key `Ctrl-H')
            2 - demo mode
          stacks: {dict}
            current dict of game.s, game.sg, game.allstacks
          level: {int}
            hint level
        """
        assert isinstance(stacks, dict) and level in (0, 1, 2)
        self._level, self._stacks = level, stacks
        self.reset()

#    def __del__(self):
#        """
#        del(self)
#        """
#        self.reset()

    def reset(self):
        """
        reset() -> None
        """
        gc.collect()
        self._color = None
        self._score = 0
        self._hints = []
        #self._max_score = 0
        #self.__clones = []

    def set_color(self, color):
        """
        set_color(color:str) -> None

        Sets score **color** when **level** is 1 (debug). Raises **ValueError**
        when **color** is other than 'red', 'black', 'blue' on None.
        """
        if not self._level == 1:
            self._color = None
        if color in ('red', 'blue', 'black', None):
            self._color = color
        else:
            raise ValueError('{0} is not an accepted color'.format(color))

    def add_hint(self, ncards, from_stack, to_stack, **kw):
        """
        add_hint(ncards:int, from_stack:Stack, to_stack:Stack
                 [, forced_move:AHINT=None]) -> None

        Add **AHINT** to **hints**.
        """
        if self._score < 0:
            return
        LOGGER.debug("SCORE: %d, NCARDS: %d, FROM:%s, TO:%s", self._score,
                     ncards, from_stack, to_stack)
        forced_move = kw.get('forced_move')
        if isinstance(forced_move, AHINT):
            assert len(forced_move) == 7
            LOGGER.debug("->SCORE: %d, NCARDS: %d, FROM:%s, TO:%s",
                         forced_move.score, forced_move.ncards,
                         forced_move.from_stack, forced_move.to_stack)
        else:
            assert forced_move is None
        color = self._color or 'black'
        assert color in ('red', 'blue', 'black', None)
        #self._max_score = max(self.max_score, score)
        #score = int(score / 10000) if self._level == 0 else score #flatten ???

        self._hints.append(AHINT(score=int(self._score), pos=-len(self._hints),
                              ncards=ncards, from_stack=from_stack,
                              to_stack=to_stack, color=color,
                              forced_move=forced_move,))

    def get(self, taken_hint=None):
        """
        get(taken_hint:AHINT=None) -> seq(AHINT ...)

        Calculate possible moves for the current position.

        Resets and reutrns **hints**.
          1) forced moves from prior hint have priority
          2) demo mode should flip cards when possible
          3) query **compute_hints** for moves possible
          4) demo mode should deal cards when possible
        """
        # 0) reset atomic move hint
        self.reset()
        talon, allstacks = self._stacks['set']['talon'], self._stacks['all']
        # 1) forced moves from prior hint have priority
        if taken_hint and taken_hint.forced_move:
            return [taken_hint.forced_move]
        # 2) demo mode should flip cards when possible
        if self._level >= 2:
            assert 0 <= self.SCORE_FLIP <= 100000
            for pile in (s for s in allstacks if s.cardFlips()):
                self._score = self.SCORE_FLIP
                self.add_hint(ncards=1, from_stack=pile, to_stack=pile)
                if self.SCORE_FLIP >= 90000:
                    self._hints.sort()
                    self._hints.reverse()
                    return self._hints
        # 3) query **compute_hints** for moves possible
        self.compute_hints()
        # 4) demo mode should deal cards when possible
        if self._level >= 2:  # dem mode
            assert 0 <= self.SCORE_DEAL <= 100000
            if talon and talon.can_deal_cards():
                self._score = self.SCORE_DEAL
                self.add_hint(ncards=0, from_stack=talon, to_stack=None)
        self._hints.sort()
        self._hints.reverse()
        return self._hints

    def compute_hints(self):
        """
        compute_hints() -> None

        Calculate and score possible moves.

        Main Intelligence. Stub for subclass.
        """
        pass

    def shall_move_pile(self, from_stack, to_stack, pile, rpile):
        """
        shall_move_pile(from_stack:Stack, to_stack:Stack, pile:seq(Card ...),
                         rpile:seq(Card ...)) -> bool

        Returns **pile** builds on **to_stack**?

        Calculate whether **pile** from **from_stack** builds on **to_stack**.
        """
        return (from_stack is not to_stack and
                to_stack.cardsBuild(from_stack, pile))

    def _seq_builds_iter(self, stacks, from_stack, pile):
        """
        _seq_builds_iter(stacks:seq(Stack ...), from_stack:Stack,
                        pile:seq(Card ...)) -> iter

        Returns a **iter** of **stacks** which build from **pile** on
        **from_stack**.
        """
        return (s for s in stacks if s is not from_stack and
                                     s.cardsBuild(from_stack, pile))

    def _seq_plays_iter(self, stacks):
        """
        _seq_plays_iter(stacks:seq(Stack ...)) -> iter

        Returns a **iter** of **stacks** which plays their top card.
        """
        return (s for s in stacks if s.cards and s.cardsPlay(s.cards[-1:]))


class DefaultHint(AbstractHint):
    """
    Calculate moves without **PREFER_HIGH_RANK_MOVES**. Optimized for Klondike
    type game, These methods deal well with other simple variants, but
    completely lacks specific strategy.
    """

    # Basic bonus for moving a card. total bonus (0..999)
    BONUS_DROP_CARD = 300
    BONUS_SAME_SUIT_MOVE = 200
    BONUS_NORMAL_MOVE = 100

    # Special bonus for flip a card or clear a pile after move. (0..9000)
    BONUS_FLIP_CARD = 1500
    BONUS_CREATE_EMPTY_ROW = 9000

    # Special bonus for moving pile. (0..4000)
    BONUS_CAN_DROP_ALL_CARDS = 4000
    BONUS_CAN_CREATE_EMPTY_ROW = 2000

    PREFER_HIGH_RANK_MOVES = False

    def __score_move_bonus(self, from_stack, to_stack, pile, rpile):
        """
        __score_move_bonus(from_stack:Stack, to_stack:Stack,
                              pile:seq(Card ...), rpile:seq(Card ...)) -> bool

        Bonus score for card move. (100 ... 999)

        Returns points for ability to drop next card and top cards values.

          *BONUS_DROP_CARD*:
            Top card on *rpile* can drop on Foundations.
          *BONUS_SAME_SUIT_MOVE*:
            Base card suit on *pile* matches top card suit on *to_stack*. Bonus
            for high rank.
          *BONUS_NORMAL_MOVE*:
            - Applied when  **PREFER_HIGH_RANK_MOVES**. Bonus for high rank.
            - Applied when *rpile* not empty. Bonus for low rank.
        """
        assert pile
        bonus, card = 0, pile[0]
        card_rank = 1 + card['rank']
        if rpile:
            fc_stack = AClonedStack.clone(from_stack, cards=rpile)
            if fc_stack.cardsDrop(self._stacks['set']['foundations'])[0]:
                bonus += self.BONUS_DROP_CARD
        if to_stack.cards and to_stack.TOPCARD['suit'] == card['suit']:
            bonus += self.BONUS_SAME_SUIT_MOVE + card_rank
        elif self.PREFER_HIGH_RANK_MOVES:
            bonus += self.BONUS_NORMAL_MOVE + card_rank
        elif rpile:
            bonus += self.BONUS_NORMAL_MOVE + (self.K - rpile[-1]['rank'])
        else:
            bonus += self.BONUS_NORMAL_MOVE + card_rank
        self._score += max(0, min(bonus, 999))

    def __score_open_bonus(self, from_stack, to_stack, pile, rpile):
        """
        __score_open_bonus(from_stack:Stack, to_stack:Stack,
          pile:seq(Card ...), rpile:seq(Card ...)) -> None

        Bonus score for stack open. (1000 ... 9999)

        Returns points for ability to flip next card and empty stack.

          *BONUS_CREATE_EMPTY_ROW*:
            - Empty **rpile** after move.
            - Empty **rpile** after all drop. (Not implemented)
            - Empty **rpile** after all play.
          *BONUS_FLIP_CARD*:
            - Top card of **rpile** is face-down and will flip up.
          *BONUS_CAN_DROP_ALL_CARDS*:
            - All cards of **rpile** may drop on next moves.

        FIX-ME: Need routine to check rpile can all drop without cheat
        """
        bonus = 0
        # check if we will create an empty row
        if not rpile:
            bonus += self.BONUS_CREATE_EMPTY_ROW
        # check if the card below the pile can be flipped
        elif not rpile[-1].face_up and pile:
            bonus += max(self.BONUS_FLIP_CARD - len(rpile), 0)
        # check if all the cards below our pile can drop
        # self.set_color('red')
        # bonus += (self.BONUS_CAN_DROP_ALL_CARDS +
        #           self.BONUS_CAN_CREATE_EMPTY_ROW)
        # check if all the cards below our pile can play
        elif from_stack.cardsPlay(rpile):
            for row in (r for r in self._stacks['set']['rows']
                          if not r is to_stack or r.cards):
                if row.cardsBuild(from_stack, rpile):
                    LOGGER.info("BONUS Clear Row. %s %s (%s/%s)", from_stack,
                                row, pile, rpile)
                    self.set_color('blue')
                    bonus += self.BONUS_CAN_CREATE_EMPTY_ROW
                    break
        self._score += max(0, min(bonus, 9000))

    def score_move_pile(self, from_stack, to_stack, pile, rpile):
        """
        score_move_pile(from_stack:Stack, to_stack:Stack, pile:seq(Card ...),
          rpile:seq(Card ...)) -> None

        Score moving a pile from stack to to stack.
        """
        assert pile
        self.__score_move_bonus(from_stack, to_stack, pile, rpile)
        self.__score_open_bonus(from_stack, to_stack, pile, rpile)

    def score_move_waste(self, from_stack, to_stack, pile, rpile):
        """
        score_move_waste(from_stack:Stack, to_stack:Stack, pile:seq(Card ...),
          rpile:seq(Card ...)) -> None

        Score moving a pile (usually 1 card) from Waste.
        """
        assert pile
        self._score = 31000 if to_stack.cards else 30000
        self.__score_move_bonus(from_stack, to_stack, pile, rpile)

    def score_drop_card(self, from_stack, to_stack, ncards):
        """
        score_drop_card(from_stack:Stack, to_stack:Stack, ncards:int) -> None

        Score drop card move.

        Score drop **pile** of **ncards** from **from_stack** to **to_stack**.
          - Many **ncards** as in spider encourage drop.
          - One **ncards** as in most games consider rank and location.
            1) ACE and TWO encourage drop.
            2) **from_stack** is **Talons** prefer **score_move_waste**.
            3) Drop clear **from_stack** encourage drop.
            4) Suggest drop as cards maybe needed elsewhere.
        """
        assert from_stack is not to_stack
        talon_group = self._stacks['grp']['Talons']
        if ncards > 1:  # drop immediately (Spider)
            self._score = 93000
            return
        from_rank = from_stack.TOPCARD['rank']
        base_rank = to_stack.cap['base_rank']
        mod = to_stack.cap['mod']
        # compute distance from **base_rank**
        tncards = (len(to_stack.cards) if base_rank < ACE
                                       else (from_rank - base_rank) % mod)
        tncards -= mod if tncards > mod / 2 else 0
        if abs(tncards) <= TWO:           # drop Ace and TWO immediately
            self._score = 92000
        elif from_stack in talon_group:   # less than score_move_waste()
            self._score = 25000
        elif len(from_stack.cards) == 1:  # Clears after drop immediately
            self._score = 91000
        # check if all the cards below our pile can drop
        # FIX-ME: Need routine to check rpile can all drop
        # self._score = 90000
        # self.set_color('red')
        else:                             # Cards maybe need elsewhere
            self._score = 50000
        self._score += (self.K - from_rank)

    def pile_sequences(self, stack):
        """
        pile_sequences(stack:Stack)) -> seq(seq(Card ...) ...)

        Returns piles that play from **stack**.

        Default ask **stack** for its **SEQUENCE**.
        """
        return (stack.SEQUENCE, )

    def pile_builds(self, from_stacks, to_stacks):
        """
        pile_builds(from_stacks:seq(Stack ...), to_stacks:seq(Stack ...))
          -> seq(Stack, Stack, seq(Card ...), seq(Card ...))

        Returns from_stack builds to_stack with pile and remains.

        For each **from_stacks** builds **to_stacks** with top card without
        play back.
        """
        for from_stack in self._seq_plays_iter(from_stacks):
            pile, rpile = from_stack.cards[-1:], from_stack.cards[:-1]
            fc_stack = AClonedStack.clone(from_stack, cards=rpile)
            for to_stack in self._seq_builds_iter(to_stacks, from_stack, pile):
                if (from_stack is not to_stack and
                    not fc_stack.cardsBuild(to_stack, pile)):
                    return (from_stack, to_stack, pile, rpile,)

    def compute_hints(self):
        """
        compute_hints() -> None

        Calculate and score possible moves.

        Provide main hint intelligence.
          - **rows** build/play on another or drop.
          - For demo and debug mode:
            - drop next after **rows** build/play on another.
            - **foundations** play on **rows**.
          - For user mode:
            - **rows** build/play on **reserves**.
            - **reserves** build/play on **rows**.
        """
        rows = self._stacks['set']['rows']
        drop_group = self._stacks['grp']['dropstacks']

        # 1) **rows** build/play on another or drop
        self.step010(drop_group, rows)

        if self._level >= 1:
            foundations = self._stacks['set']['foundations']
            # 2) drop next after **rows** build/play on another.
            if not self._hints:
                self.step020(rows, foundations)
            # 3) **foundations** play on **rows**.
            if not self._hints:
                self.step030(foundations, rows, drop_group)

        if self._level == 0:
            reserve_group = self._stacks['grp']['reservestacks']  # FIXME: Need?
            # 4) **rows** build on **reserves**.
            if not self._hints:
                self.step040(rows, reserve_group)
            # 5) **reserves** build on **rows**.
            if not self._hints:
                self.step050(reserve_group, rows)

    def step010(self, dropstacks, rows):
        """
        step010(dropstacks:tup, rowstacks:tup) -> None

        Calculate and score **dropstacks** drop/build **rows**. (60000 .. 99999)

        When **dropstacks** drop/build **rows**, sum score based on destination
        with **score_drop_card** or **step010_move_pile** then **add_hint** for
        each possible move unless a drop is encouraged.
        """
        foundations = self._stacks['set']['foundations']
        for from_stack in dropstacks:
            self._score = 0
            to_stack, ncards = from_stack.cardsDrop(foundations)
            if to_stack:
                self.score_drop_card(from_stack, to_stack, ncards)
                self.add_hint(ncards, from_stack, to_stack)
                if self._score >= 90000 and self._level >= 1:
                    break
            for pile in self.pile_sequences(from_stack):
                if pile:
                    self.step010_move_pile(from_stack, pile, rows)

    def step010_move_pile(self, from_stack, pile, rows):
        """
        step010_move_pile(from_stack:seq(Stack ...), pile:seq(Card ...),
                          rows:seq(Stack ...)) -> None

        Calculate and score **pile** build **rows**. (60000 .. 99999)

        When **fromstack** **pile** builds **rows**, sum score based on
        **fromstack** location and **rows** state then **add_hint** with
        **score_move_waste** or **score_move_pile** for each possible move.
        """
        ncards, rcards = len(pile), len(from_stack.cards)
        assert 1 <= ncards <= rcards
        rpile = from_stack.cards[:-rcards]  # remaining pile
        empty_row_seen = False
        from_waste = from_stack in self._stacks['grp']['Talons']
        for to_stack in rows:
            if not self.shall_move_pile(from_stack, to_stack, pile, rpile):
                continue
            self._score = 0
            if from_waste:  # move card from Waste
                self.score_move_waste(from_stack, to_stack, pile, rpile)
            else:
                # Limit clear stack moves and Entire stack moves on clear
                if not to_stack.cards:
                    if ncards == rcards or empty_row_seen:
                        continue
                    self._score, empty_row_seen = 60000, True
                else:
                    self._score = 80000
                self.score_move_pile(from_stack, to_stack, pile, rpile)
            self.add_hint(ncards, from_stack, to_stack)

    def step020(self, rowstacks, foundations):
        """
        step020(rows:seq(Stack ...), foundations:seq(Stack ...)) -> None

        Calculate and score drop next **rows** build another. (40000 .. 59999)

        When **rows** builds another and next **rows** drop, score based on
        cards value then **add_hint** with **forced_hint** for second move for
        each possible move.
        """

        def drop_next_iter(from_stack, pile):
            """
            Any card in **pile** drops?
            """
            for i, card in enumerate(pile, start=1):
                cf_stack = AClonedStack.clone(from_stack, cards=[card])
                stack, ncards = cf_stack.cardsDrop(foundations)
                if stack and from_stack.cardsPlay(pile[i:]):
                    yield (stack, pile[i:], )

        # Check available **rows** sequences
        for from_stack, pile in ([s, self.pile_sequences(s)]
                                    for s in rowstacks):
            if not pile or len(pile) < 2:
                continue
            # find next **dropstacks** that benefits from card
            for stack, sub_pile in drop_next_iter(from_stack, pile):
                for to_stack in self._seq_builds_iter(rowstacks, from_stack,
                                                      sub_pile):
                    self._score = 50000 + (self.K - from_stack.TOPCARD['rank'])
                    self.set_color('red')
                    self.add_hint(ncards=len(sub_pile),
                                  from_stack=from_stack, to_stack=to_stack,
                                  # Forced move helps avoid play back
                                  forced_move=(AHINT(score=999999, pos=0,
                                                 ncard=1, from_stack=from_stack,
                                                 to_stack=stack, color='red',
                                                 forced_move=None)))

    def step030(self, foundations, rows, dropstacks):
        """
        step030(foundations:seq(Stack ...), rows:seq(Stack ...),
                droptacks:seq(Stack ...)) -> None

        Calculate and score **foundations** play **rows**. (20000 .. 29999)

        When **foundations** builds **rows** and **dropstacks** play **rows**,
        sum score based on cards value then **add_hint** with **forced_hint**
        for second move for each possible move.
        """
        for stack in self._seq_plays_iter(foundations):
            card = stack.TOPCARD
            # find **rows** that accepts card
            for to_stack in self._seq_builds_iter(rows, stack, [card]):
                ct_stack = AClonedStack.clone(to_stack,
                                              cards=to_stack.cards + [card])
                # find next **dropstacks** that benefits from card
                for from_stack in (d for d in dropstacks if d is not to_stack):
                    pile = from_stack.SEQUENCE
                    if not pile or not ct_stack.cardsBuild(from_stack, pile):
                        continue
                    cf_stack = AClonedStack.clone(from_stack,
                                            cards=from_stack.cards[:-len(pile)])
                    if cf_stack.cardsBuild(to_stack, pile):
                        continue
                    self._score = 20000 + card['rank']
                    self.set_color('blue')
                    self.add_hint(ncards=1, from_stack=stack, to_stack=to_stack,
                                  # Forced move helps avoid play back
                                  forced_move=(AHINT(score=999999, pos=0,
                                        ncards=len(pile), from_stack=from_stack,
                                        to_stack=to_stack, color='blue',
                                        forced_move=None,)))

    def step040(self, rows, reserves):
        """
        step040(rows:seq(Stack ...), reserves:seq(Stack ...)) -> None

        Calculate and score **rows** build on **reserves**. (10000 .. 19999)

        When **rows** builds **reserves**, sum score with **score_move_pile**
        then **add_hint** with for first possible move.
        """
        if not reserves:
            return
        args = self.pile_builds(from_stacks=rows, to_stacks=reserves)
        if args:
            self._score = 10000
            self.score_move_pile(*args)
            self.add_hint(len(args[2]), args[0], args[1])

    def step050(self, reserves, rows):
        """
        step050(reserves:seq(Stack ...), rows:seq(Stack ...)) -> None

        Calculate and score **reserves** build on **rows**. (10000 .. 19999)

        When **reserves** builds **rows**, sum score with **score_move_pile**
        then **add_hint** with for first possible move.
        """
        if not reserves:
            return
        args = self.pile_builds(from_stacks=reserves, to_stacks=rows)
        if args:
            self._score = 10000
            self.score_move_pile(*args)
            self.add_hint(len(args[2]), args[0], args[1])


class CautiousHintMethods(object):
    """
    Methods that are more cautious checking for a play back loop within
    **shall_move_pile**.
    """

    def shall_move_pile(self, from_stack, to_stack, pile, rpile):
        """
        shall_move_pile(from_stack:Stack, to_stack:Stack, pile:seq(Card ...),
          rpile:seq(Card ...)) -> bool

        Returns **pile** builds on **to_stack**?

        Augment whether **pile** from **from_stack** builds on **to_stack** to
        calculate for possible play back of **pile**.
        """
        if not super(CautiousHintMethods, self).shall_move_pile(from_stack,
                                                       to_stack, pile, rpile):
            return False
        if not rpile:
            return True
        cf_stack = AClonedStack.clone(from_stack, cards=rpile)
        return not cf_stack.cardsBuild(to_stack, pile)


class DropbyHintMethods(object):
    """
    Methods that encourage all drops with no logic in **score_drop_card**.
    """
    _score = 0

    def score_drop_card(self, score, from_stack, to_stack, ncards):
        """
        score_drop_card(from_stack:Stack, to_stack:Stack, ncards:int) -> None

        Score drop card move.

        Overide score drop **pile** of **ncards** from **from_stack** to
        **to_stack**.
          - Encourage all drops.
        """
        self._score = 93000


class CautiousDefaultHint(CautiousHintMethods, DefaultHint):
    """
    Calculate moves that are more cautious checking for play back loops within
    **shall_move_pile** and **PREFER_HIGH_RANK_MOVES**.
    """
    PREFER_HIGH_RANK_MOVES = True


class DropbyDefaultHint(DropbyHintMethods, DefaultHint):
    """
    Calculate moves that encourage drops with no logic in **score_drop_card**.
    """

class DropbyCautiousHint(DropbyHintMethods, CautiousDefaultHint):
    """
    Calculate moves that are more cautious checking for play back loops within
    **shall_move_pile** and **PREFER_HIGH_RANK_MOVES** and encourage all drops
    with no logic in **score_drop_card**.
    """


class SortHeldCautiousHint(CautiousDefaultHint):
    """
    Calculate moves that are more cautious checking for play back loops within
    **shall_move_pile** and **PREFER_HIGH_RANK_MOVES**. Provides an additional
    move when none given that sorts cards on **from_pile** over 3 **to_pile**.
    Default is from talon to reserves.
    """

    @property
    def _to_piles(self):
        """
        _to_piles() -> seq

        Cards should be sorted to these piles. Default Game.s.reserves.
        """
        return self._stacks['set']['reserves']

    @property
    def _from_pile(self):
        """
        _from_pile() -> seq

        Rank should be found from this pile top card. Default Game.s.talon.
        """
        return self._stacks['set']['talon']

    def compute_hints(self):
        """
        compute_hints() -> None

        Calcalate possible moves.

        Augment main hint intelligence.
          - **rows** build/play on another or drop.
          - drop next after **rows** build/play on another.
          - **foundations** play on **rows**.
          - **rows** build/play on **reserves**.
          - **reserves** build/play on **rows**.
          - **from_stack** play on **to_stacks** sorted.
            - first third from foundation base rank.
            - second third from the middle ranks.
            - last third recieves the remainder of ranks.
        """
        super(SortHeldCautiousHint, self).compute_hints()
        if self._hints or not self._from_pile.cards:
            return
        # 6) **from_stack** play on **to_stacks** sorted.
        cap = self._stacks['set']['foundations'][0].cap
        rank, mod = cap['base_rank'], cap['mod']
        rank = (self._from_pile.TOPCARD['rank'] - rank) % mod
        # select **to_stack** by portion rank within.
        i = 0 if 0 <= rank <= 3 else 1 if 4 <= rank <= 7 else 2
        self._score = 5000
        self.add_hint(1, self._from_pile, self._to_piles[i])


class MoveHint(object):
    """
    Generate possible move hint for player hint or demo move
    """
    __slots__ = ('__list', '__index', '__level', '__stacks', '_hint_')

    def __init__(self, game, cls=None):
        """
        Initialize hints. Initialize data for Hint. Set internal stacks with
        stack lists from Game and internal hint for calculating moves.

        .. \__init__(game:Game[, cls:Hint]) -> None
        """
        self.__list = self.__index = self.__level = None
        self._hint_ = cls
        self.__stacks = {'set': game.s, 'grp': game.sg, 'all': game.allstacks}

    @property
    def is_set(self):
        """
        Return whether hint class is set. Checked by menu show pile hint.

        .. is_set -> bool
        """
        return self._hint_ is not None

    def reset(self):
        """
        Reset calculated moves.

        .. reset() -> None
        """
        self.__list = None

    def _assert(self, move):
        """
        Paranoia - verify calculated moves for current level and actual ability
        to perform move. Raises AssertionError for any failure.

        .. \_assert(move:Tuple) -> None
        """
        talon, level = self.__stacks['set']['talon'], self.__level
        if move is None:
            return
        # a) Any hint must come from_stack with cards more than move.
        assert move.from_stack and len(move.from_stack.cards) >= move.ncards
        LOGGER.debug("HINT from %s to %s with %s", move.from_stack,
                     move.to_stack, move.ncards)
        # b) All Deal hints must come from talon while in demo mode.
        if move.ncards == 0:
            assert level >= 2 and move.from_stack is talon
        # c) All Flip hints must be on the same stack while in demo mode.
        elif move.from_stack == move.to_stack:  # Flip move. Demo with 1 card.
            assert level >= 2 and move.ncards == 1
        # d) All other Move hints must build from stack cards on to stack.
        else:
            assert (move.to_stack and move.ncards >= 1 and
                    move.to_stack.cardsBuild(move.from_stack,
                                          move.from_stack.cards[-move.ncards:]))

    def get(self, level=0, taken_hint=None):
        """
        Get next calculated move. Return calcutated move from hint.

        .. get([level:int, taken_hint:HintTuple) -> HintTuple
        """
        assert type(level) is int
        if self._hint_ is None:
            return
        # reset list if level has changed
        if level != self.__level:
            self.__level, self.__list = level, None
        # compute all hints
        if self.__list is None:
            move_hint = self._hint_(self.__stacks, level)
            self.__list = move_hint.get(taken_hint)
            LOGGER.debug("Show card hints: %r", self.__list)
            self.__index = 0
        # get next hint from list
        if len(self.__list) == 0:
            return
        move_hint = self.__list[self.__index]
        self._assert(move_hint)
        self.__index += 1
        self.__index %= len(self.__list)
        return move_hint

    def __repr__(self):
        """
        Returns current hint name with current list of calculated moves.
        """
        return '%s (%r)' % (self._hint_.__name__, self.__list)